동적 메모리 실습 — malloc·free 오류 패턴과 유연 배열 구조체 연결리스트

NOTE

C 기초 강의의 동적 메모리 실습(9-01 ~ 9-11)에서 만난 오류 패턴을 정리하고, 이를 바탕으로 realloc 기반 문자열 배열과 유연 배열 멤버(flexible array member) 노드를 쓰는 연결리스트를 직접 만들어 본다.

환경은 Linux 컨테이너, gcc -g -O0 -Wall -Wextra.

IMPORTANT

코드는 컨테이너에서 컴파일·실행해 확인했고, 설명은 AI(Claude)와 함께 코드 리뷰하며 정리했다.

TIP

📂 원본 소스 (GitHub)

📌 한눈에 보기

실습문제처방
9-01같은 포인터를 두 곳에서 free (double free)해제 책임자를 한 곳으로, free 후 NULL 대입
9-03잘못된 타입 크기로 할당malloc(sizeof(*p))
9-04malloc(0)·음수 크기크기는 size_t, 0 허용 여부를 명시적으로 처리
9-05calloc 개수×크기 오버플로, free 누락곱셈 한계 검사, 반드시 해제
9-06malloc 직후 쓰레기값calloc 또는 memset, 읽는 크기는 할당 크기 이내
9-07에러 경로마다 해제 코드가 흩어짐goto 한 곳으로 모으는 정리 패턴
9-08free(p) 후 p->next 참조다음 노드를 먼저 저장
9-09에러·정상 경로에서 free 누락단일 출구 정리
9-10유연 배열 구조체 복사·스택 선언·값 전달memcpy로 전체 크기 복사, 포인터로 전달
9-11구조체 포인터의 크기로 할당sizeof(*tmb)

1. free 관련 오류

double free — 9-01

요약

  • 문제: 검증 함수와 호출한 함수가 같은 포인터를 두 번 free
  • 원인: 해제 책임자가 둘로 나뉨
  • 처방: 할당한 쪽만 해제하고 검증 함수는 판단만, free 후 NULL 대입 (FREE 매크로는 do { } while (0)로 정의)

free 후 참조 — 9-08 (치명적)

요약

  • 문제: for (p = head; p; p = p->next) free(p);가 해제된 노드의 next를 읽음 (운영 장애 사례)
  • 원인: free(p) 뒤 p->next 접근. 테스트에서는 이전 값이 남아 통과하기 쉬움
  • 처방: 해제 전에 next = p->next를 먼저 저장

해제 누락 — 9-05, 9-09

요약

  • 문제: 에러·정상 경로에서 free 누락 (9-05, 9-09)
  • 원인: fopen/fgets 실패 경로에서 text_buffer를 해제하지 않음
  • 처방: 빠져나가는 모든 경로에서 해제 → 단일 출구 정리(§3)

2. 할당 크기 관련 오류

타입을 잘못 계산 — 9-03, 9-11

요약

  • 문제: malloc(sizeof(gadget)), malloc(sizeof(tmb))로 작게 할당해 힙 오버플로 (9-03, 9-11)
  • 원인: sizeof(tmb)는 포인터 크기(8바이트)
  • 처방: p = malloc(sizeof(*p)) 관용구 (이 저장소의 XNEW/XNEWN도 같은 방식)
  • 결과: struct tm의 tm_year는 1900년 기준, tm_mon은 0~11

0과 음수 — 9-04, 9-05

요약

  • 문제: malloc(0)·음수 크기·calloc 곱셈 오버플로 (9-04, 9-05)
  • 원인: malloc(0)은 구현 정의, int 음수는 size_t에서 거대한 값이 됨
  • 처방: 크기 인자는 size_t로 받고 호출 전에 범위·곱셈 한계 검사. malloc(0) 결과는 역참조하지 않음

초기화와 읽기 범위 — 9-06

요약

  • 문제: malloc 직후 쓰레기값을 읽고, 할당 크기를 넘겨 읽음 (9-06)
  • 원인: malloc은 초기화하지 않음
  • 처방: calloc/memset, 읽는 크기는 할당 크기 이내. 비밀값 버퍼는 해제 전에 지움

3. goto로 자원 정리 — 9-07

요약

  • 문제: 에러 경로가 늘수록 free/fclose를 하나씩 빼먹음 (9-07)
  • 원인: 경로마다 정리 코드가 흩어짐
  • 처방: 초기값을 NULL로 잡고 CLEAN: 구간 하나로 모음. 정리는 획득의 역순, errno는 실패 직후 저장

4. 유연 배열 멤버(Flexible Array Member) — 9-10

flowchart LR
    subgraph BLOCK["malloc(sizeof(구조체) + sizeof(int) * 4)"]
        direction LR
        H["num<br/>8바이트<br/>sizeof에 포함"] --- D0["data[0]"] --- D1["data[1]"] --- D2["data[2]"] --- D3["data[3]"]
    end

a. 구조체 대입은 헤더만 복사한다

요약

  • 문제: *b = *a로 복사하면 data[]가 빠짐
  • 원인: 대입은 sizeof에 잡히는 헤더만 복사
  • 처방: sizeof(*a) + sizeof(int) * a->num 만큼 memcpy (대상에도 같은 크기를 할당)

b. 스택 변수로 선언하면 data[]의 공간이 없다

요약

  • 문제: 스택에 선언한 유연 배열 구조체의 data[i] 쓰기가 스택을 오염
  • 원인: 구조체는 8바이트뿐이고 data[] 공간이 없음
  • 처방: 반드시 malloc 등으로 크기를 계산해 할당

c. 값으로 전달하면 data[]가 사라진다

요약

  • 문제: 값으로 넘긴 복사본에는 data[]가 없음
  • 원인: 값 전달은 헤더만 복사
  • 처방: const struct flex_array_struct *p로 포인터 전달

5. 직접 만들기 1 — realloc과 이중 포인터

요약

  • 문제: 모자라면 늘리는 append에서 realloc 실패 시 원본 소실, 이어 붙일 위치 오류
  • 원인: realloc은 주소를 바꿀 수 있고, 반환값을 원래 포인터에 바로 대입하면 NULL 때 누수
  • 처방: 이중 포인터 Array **plist로 받고 임시 변수 tmp로 검사 후 대입, 위치는 dst_len, 초기 상태는 빈 문자열
  • 결과: strlen 루프 조건은 count < size && str_chr[count] != '\0'

6. 직접 만들기 2 — 연결리스트 (node + list 구조체)

flowchart LR
    L["msg_list_t<br/>head · tail · count"] -- "head" --> N1["node<br/>next · len · data[]"]
    N1 -- "next" --> N2["node"]
    N2 -- "next" --> N3["node"]
    N3 -- "next" --> NIL(["NULL"])
    L -- "tail" --> N3

insert — tail 덕분에 O(1)

요약

  • 문제: 새 노드 할당 크기와 next 초기화 오류
  • 원인: 유연 배열 노드 크기 계산이 틀리고 next가 쓰레기값
  • 처방: sizeof(msg_node_t) + len + 1, next = NULL, list->tail로 O(1) 삽입

delete — head/tail까지 갱신

요약

  • 문제: 첫·마지막 노드 삭제에서 head/tail 미갱신
  • 원인: pre_node가 없는 경우와 target == tail 경우를 구분하지 않음
  • 처방: pre_node = NULL로 시작해 경우를 나누고 tail도 갱신, index <= 0 || index > count 검사

free_list

처음 짠 연결리스트에서 고친 점

실행 결과


✅ 정리 — 동적 메모리 점검 체크리스트